/*
 * Copyright 2002-2010 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

using Microsoft.Extensions.Logging;
using Spring.Context;
using Spring.Messaging.Ems.Common;
using Spring.Messaging.Ems.Connections;
using Spring.Messaging.Ems.Support;
using Spring.Messaging.Ems.Support.Destinations;
using Spring.Objects.Factory;

namespace Spring.Messaging.Ems.Listener;

/// <summary>
/// Common base class for all containers which need to implement listening
/// based on a Connection (either shared or freshly obtained for each attempt).
/// Inherits basic Connection and Session configuration handling from the
/// <see cref="EmsAccessor"/> base class.
/// </summary>
/// <para>
/// This class provides basic lifecycle management, in particular management
/// of a shared Connection. Subclasses are supposed to plug into this
/// lifecycle, implementing the <see cref="SharedConnectionEnabled"/> as well as
///
/// </para>
/// <remarks>
///
/// </remarks>
/// <author>Mark Pollack</author>
public abstract class AbstractListenerContainer : EmsDestinationAccessor, ILifecycle, IObjectNameAware, IDisposable
{
    private readonly ILogger<AbstractListenerContainer> logger = LogManager.GetLogger<AbstractListenerContainer>();

    private String clientId;

    private bool autoStartup = true;

    private string objectName;

    private IConnection sharedConnection;

    private bool sharedConnectionStarted = false;

    /// <summary>
    /// The monitor object to lock on when performing operations on the connection.
    /// </summary>
    protected object sharedConnectionMonitor = new object();

    private volatile bool active = false;

    private bool running = false;

    /// <summary>
    /// The monitor object to lock on when performing operations that update the lifecycle of the container.
    /// </summary>
    protected object lifecycleMonitor = new object();

    /// <summary>
    /// Gets or sets the client id for a shared Connection created and used by this container.
    /// </summary>
    /// <remarks>
    /// Note that client ids need to be unique among all active Connections
    /// of the underlying JMS provider. Furthermore, a client id can only be
    /// assigned if the original ConnectionFactory hasn't already assigned one.
    /// </remarks>
    /// <value>The client id.</value>
    public string ClientId
    {
        set { clientId = value; }
        get { return clientId; }
    }

    /// <summary> Set whether to automatically start the listener after initialization.
    /// <p>Default is "true"; set this to "false" to allow for manual startup.</p>
    /// </summary>
    public virtual bool AutoStartup
    {
        set { this.autoStartup = value; }
    }

    /// <summary>
    /// Set the name of the object in the object factory that created this object.
    /// </summary>
    /// <value>The name of the object in the factory.</value>
    /// <remarks>
    /// 	<p>
    /// Invoked after population of normal object properties but before an init
    /// callback like <see cref="Spring.Objects.Factory.IInitializingObject"/>'s
    /// <see cref="Spring.Objects.Factory.IInitializingObject.AfterPropertiesSet"/>
    /// method or a custom init-method.
    /// </p>
    /// </remarks>
    public string ObjectName
    {
        set { objectName = value; }
    }

    /// <summary>
    /// Gets a value indicating whether this container is currently running,
    /// that is, whether it has been started and not stopped yet.
    /// </summary>
    /// <value>
    /// 	<c>true</c> if this container is running; otherwise, <c>false</c>.
    /// </value>
    public bool IsRunning
    {
        get
        {
            lock (lifecycleMonitor)
            {
                return (running && RunningAllowed);
            }
        }
    }

    /// <summary>
    /// Gets a value indicating whether this container's listeners are generally allowed to run.
    /// </summary>
    /// <remarks>
    /// <para>
    /// >This implementation always returns <code>true</code>; the default 'running'
    /// state is purely determined by <see cref="Start"/>/<see cref="Stop"/>.
    /// </para>
    /// <para>
    /// Subclasses may override this method to check against temporary
    /// conditions that prevent listeners from actually running. In other words,
    /// they may apply further restrictions to the 'running' state, returning
    /// <code>false</code> if such a restriction prevents listeners from running.
    /// </para>
    /// </remarks>
    /// <value><c>true</c> if running allowed; otherwise, <c>false</c>.</value>
    protected virtual bool RunningAllowed
    {
        get
        {
            return true;
        }
    }

    /// <summary>
    /// Gets a value indicating whether	this container is currently active,
    /// that is, whether it has been set up but not shut down yet.
    /// </summary>
    /// <value><c>true</c> if active; otherwise, <c>false</c>.</value>
    public virtual bool Active
    {
        get
        {
            lock (this.lifecycleMonitor)
            {
                return this.active;
            }
        }
    }

    /// <summary> Return whether a shared EMS Connection should be maintained
    /// by this listener container base class.
    /// </summary>
    /// <seealso cref="SharedConnection"/>
    protected abstract bool SharedConnectionEnabled { get; }

    /// <summary>
    /// Gets the shared connection maintained by this container.
    /// Available after initialization.
    /// </summary>
    /// <value>The shared connection (never null)</value>
    /// <exception cref="InvalidOperationException">if this container does not maintain a
    /// shared Connection, or if the Connection hasn't been initialized yet.
    /// </exception>
    /// <see cref="SharedConnectionEnabled"/>
    protected IConnection SharedConnection
    {
        get
        {
            if (!SharedConnectionEnabled)
            {
                throw new InvalidOperationException("This listener container does not maintain a shared Connection");
            }

            lock (this.sharedConnectionMonitor)
            {
                if (this.sharedConnection == null)
                {
                    throw new SharedConnectionNotInitializedException("This listener container's shared Connection has not been initialized yet");
                }

                return this.sharedConnection;
            }
        }
    }

    /// <summary>
    /// Call base class method, then <see cref="ValidateConfiguration"/> and then <see cref="Initialize"/>
    /// </summary>
    public override void AfterPropertiesSet()
    {
        base.AfterPropertiesSet();
        ValidateConfiguration();
        Initialize();
    }

    /// <summary>
    /// Validates the configuration of this container.  The default implementation
    /// is empty.  To be overriden in subclasses.
    /// </summary>
    protected virtual void ValidateConfiguration()
    {
    }

    /// <summary>
    /// Calls <see cref="Shutdown"/> when the application context destroys the container instance.
    /// </summary>
    public void Dispose()
    {
        Shutdown();
    }

    /// <summary>
    /// Initializes this container.  Creates a Connection, starts the Connection
    /// (if the property <see cref="AutoStartup"/> hasn't been turned off), and calls
    /// <see cref="DoInitialize"/>.
    /// </summary>
    /// <exception cref="EMSException">If startup failed</exception>
    public virtual void Initialize()
    {
        try
        {
            lock (this.lifecycleMonitor)
            {
                this.active = true;
                System.Threading.Monitor.PulseAll(this.lifecycleMonitor);
            }

            if (this.autoStartup)
            {
                DoStart();
            }

            DoInitialize();
        }
        catch (Exception)
        {
            lock (this.sharedConnectionMonitor)
            {
                ConnectionFactoryUtils.ReleaseConnection(sharedConnection, ConnectionFactory, autoStartup);
            }

            throw;
        }
    }

    /// <summary>
    /// Stop the shared connection, call <see cref="DoShutdown"/>, and close this container.
    /// </summary>
    public virtual void Shutdown()
    {
        logger.LogDebug("Shutting down message listener container");
        bool wasRunning = false;
        lock (this.lifecycleMonitor)
        {
            wasRunning = this.running;
            this.running = false;
            this.active = false;
            System.Threading.Monitor.PulseAll(this.lifecycleMonitor);
        }

        if (wasRunning && SharedConnectionEnabled)
        {
            try
            {
                StopSharedConnection();
            }
            catch (Exception ex)
            {
                logger.LogDebug(ex, "Could not stop EMS Connection on shutdown");
            }
        }

        // Shut down the invokers
        try
        {
            DoShutdown();
        }
        finally
        {
            lock (this.sharedConnectionMonitor)
            {
                ConnectionFactoryUtils.ReleaseConnection(this.sharedConnection, ConnectionFactory, false);
            }
        }
    }

    /// <summary>
    /// Starts this container.
    /// </summary>
    /// <exception cref="EMSException">if starting failed.</exception>
    public void Start()
    {
        DoStart();
    }

    /// <summary>
    /// Start the shared Connection, if any, and notify all invoker tasks.
    /// </summary>
    protected virtual void DoStart()
    {
        // Lazily establish a shared Connection, if necessary.
        if (SharedConnectionEnabled)
        {
            EstablishSharedConnection();
        }

        lock (this.lifecycleMonitor)
        {
            running = true;
            System.Threading.Monitor.PulseAll(this.lifecycleMonitor);
        }

        // Start the shared Connection, if any.
        if (SharedConnectionEnabled)
        {
            StartSharedConnection();
        }
    }

    /// <summary>
    /// Stops this container.
    /// </summary>
    /// <exception cref="EMSException">if stopping failed.</exception>
    public void Stop()
    {
        DoStop();
    }

    /// <summary>
    /// Notify all invoker tasks and stop the shared Connection, if any.
    /// </summary>
    /// <exception cref="EMSException">if thrown by EMS API methods.</exception>
    /// <see cref="StopSharedConnection"/>
    protected virtual void DoStop()
    {
        lock (this.lifecycleMonitor)
        {
            this.running = false;
            System.Threading.Monitor.PulseAll(this.lifecycleMonitor);
        }

        if (SharedConnectionEnabled)
        {
            StopSharedConnection();
        }
    }

    /// <summary>
    /// Register any invokers within this container.
    /// Subclasses need to implement this method for their specific
    /// invoker management process.  A shared Connection, if any, will already have been
    /// started at this point.
    /// </summary>
    protected abstract void DoInitialize();

    /// <summary>
    /// Close the registered invokers.  Subclasses need to implement this method
    /// for their specific invoker management process. A shared Connection, if any,
    /// will automatically be closed afterwards.
    /// </summary>
    protected abstract void DoShutdown();

    /// <summary>
    /// Establishes a shared Connection for this container.
    /// </summary>
    /// <remarks>
    /// <para>
    /// The default implementation delegates to <see cref="CreateSharedConnection"/>
    /// which does one immediate attempt and throws an exception if it fails.
    /// Can be overridden to have a recovery process in place, retrying
    /// until a Connection can be successfully established.
    /// </para>
    /// </remarks>
    /// <exception cref="EMSException">If thrown by EMS API methods</exception>
    protected virtual void EstablishSharedConnection()
    {
        lock (sharedConnectionMonitor)
        {
            if (sharedConnection == null)
            {
                sharedConnection = CreateSharedConnection();
                logger.LogDebug("Established shared EMS Connection");
            }
        }
    }

    /// <summary>
    /// Refreshes the shared connection that this container holds.
    /// </summary>
    /// <remarks>
    /// Called on startup and also after an infrastructure exception
    /// that occurred during invoker setup and/or execution.
    /// </remarks>
    /// <exception cref="EMSException">If thrown by EMS API methods</exception>
    protected void RefreshSharedConnection()
    {
        lock (sharedConnectionMonitor)
        {
            ConnectionFactoryUtils.ReleaseConnection(sharedConnection, ConnectionFactory, sharedConnectionStarted);
            sharedConnection = CreateSharedConnection();
            if (sharedConnectionStarted)
            {
                sharedConnection.Start();
            }
        }
    }

    /// <summary>
    /// Creates the shared connection for this container.
    /// </summary>
    /// <remarks>
    /// The default implementation creates a standard Connection
    /// and prepares it through <see cref="PrepareSharedConnection"/>
    /// </remarks>
    /// <returns>the prepared Connection</returns>
    /// <exception cref="EMSException">if the creation failed.</exception>
    protected virtual IConnection CreateSharedConnection()
    {
        IConnection con = CreateConnection();
        try
        {
            PrepareSharedConnection(con);
            return con;
        }
        catch (EMSException)
        {
            EmsUtils.CloseConnection(con);
            throw;
        }
    }

    /// <summary>
    /// Prepares the given connection, which is about to be registered
    /// as shared Connection for this container.
    /// </summary>
    /// <remarks>
    /// The default implementation sets the specified client id, if any.
    /// Subclasses can override this to apply further settings.
    /// </remarks>
    /// <param name="connection">The connection to prepare.</param>
    /// <exception cref="EMSException">If the preparation efforts failed.</exception>
    protected virtual void PrepareSharedConnection(IConnection connection)
    {
        if (ClientId != null)
        {
            connection.ClientID = ClientId;
        }
    }

    /// <summary>
    /// Starts the shared connection.
    /// </summary>
    /// <exception cref="EMSException">If thrown by EMS API methods</exception>
    /// <see cref="Start"/>
    protected virtual void StartSharedConnection()
    {
        lock (sharedConnectionMonitor)
        {
            if (sharedConnection != null)
            {
                try
                {
                    sharedConnectionStarted = true;
                    sharedConnection.Start();
                }
                catch (Exception ex)
                {
                    logger.LogWarning(ex, "Ignoring Connection start exception - assuming already started");
                }
            }
        }
    }

    /// <summary>
    /// Stops the shared connection.
    /// </summary>
    /// <exception cref="EMSException">if thrown by EMS API methods.</exception>
    protected virtual void StopSharedConnection()
    {
        lock (this.sharedConnectionMonitor)
        {
            if (this.sharedConnection != null)
            {
                try
                {
                    this.sharedConnectionStarted = false;
                    this.sharedConnection.Stop();
                }
                catch (System.InvalidOperationException ex)
                {
                    logger.LogWarning(ex, "Ignoring Connection stop exception - assuming already stopped");
                }
            }
        }
    }
}

/// <summary>
/// Exception that indicates that the initial setup of this container's
/// shared Connection failed. This is indicating to invokers that they need
/// to establish the shared Connection themselves on first access.
/// </summary>
public class SharedConnectionNotInitializedException : EMSException
{
    /// <summary>
    /// Initializes a new instance of the <see cref="SharedConnectionNotInitializedException"/> class.
    /// </summary>
    /// <param name="message">The message.</param>
    public SharedConnectionNotInitializedException(string message) : base(message)
    {
    }
}
